/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 *
 */

import os from 'os';
import path from 'path';

import { hvigorCore, MetricLogType } from '@ohos/hvigor';
import fse from 'fs-extra';

import { NativeCommandBuilder } from '../builder/native-command-builder.js';
import { ohosTrace } from '../common/trace/ohos-trace.js';
import { CommonConst, NativeConst } from '../const/common-const.js';
import { ApiVersion } from '../const/sdk-const.js';
import { HvigorNativeConfigFile, readHvigorNativeConfigFile } from '../model/cxx/hvigor-native-build.js';
import { ProjectBuildProfile } from '../options/build/project-build-profile.js';
import { createInvalidationState, FileId, readCMakeFileApiReply } from '../utils/cmake-invalidation-state.js';
import { CmakeUtil } from '../utils/cmake-util.js';
import { FileUtil } from '../utils/file-util.js';
import { OhosLogger } from '../utils/log/ohos-logger.js';
import { AbstractBuildNative } from './abstract-build-native.js';
import { TaskNames } from './common/task-names.js';
import { LegacyPreBuild } from './legacy-tasks/legacy-pre-build.js';
import { PreBuild } from './pre-build.js';
import { TargetTaskService } from './service/target-task-service.js';

import Task = TaskNames.Task;

/**
 * ohos native代码编译任务
 *
 * @since 2021/1/21
 */
export class BuildNativeWithCmake extends AbstractBuildNative {
  private _log: OhosLogger = OhosLogger.getLogger(BuildNativeWithCmake.name);
  private readonly _moduleDir: string;
  private static readonly sanAbilitySet = new Set(['-DOHOS_ENABLE_ASAN=ON', '-DOHOS_ENABLE_TSAN=ON', '-DOHOS_ENABLE_HWASAN=ON', '-DOHOS_ENABLE_UBSAN=ON']);

  constructor(taskService: TargetTaskService) {
    super(taskService, Task.BUILD_NATIVE_WITH_CMAKE);
    this._moduleDir = this.moduleModel.getProjectDir();
  }

  initTaskDepends(): void {
    this.declareDepends(this.isFaMode ? LegacyPreBuild.name : PreBuild.name);
  }

  taskShouldDo(): boolean {
    return CmakeUtil.nativeTaskCondition(this.moduleModel, this.targetData, this._nativeOption);
  }

  protected async doTaskAction(): Promise<void> {
    FileUtil.checkDirWithoutDelete(this.pathInfo.getModuleBuildPath());
    FileUtil.checkDirWithoutDelete(this.pathInfo.getCppOutputDir());

    if (this._nativeOption !== undefined) {
      let cmakeListsPath = this._nativeOption.path;
      if (cmakeListsPath === undefined) {
        cmakeListsPath = './src/main/cpp/CMakeLists.txt';
        this._log.warn(`The path of the cmake script in externalNativeOptions is empty. Use the default path ${cmakeListsPath}.`);
      }
    }

    const abiFilters = CmakeUtil.checkAbiFilters(this._nativeOption?.abiFilters, this.targetData.isHarmonyOS(), this.moduleModel, this.targetName);
    for (const abiFilter of abiFilters) {
      const subTaskName = `generate CMake command with ABI of ${abiFilter}`;
      const subDurationEvent = this.durationEvent.createSubEvent(subTaskName, '');
      subDurationEvent.start();
      const workDir = path.resolve(this.pathInfo.getNinjaWorkDir(), abiFilter);
      const command: string[] = this.buildCommand(abiFilter);
      const commandFile = path.resolve(workDir, 'metadata_generation_command.txt');
      const commandText = command.join(os.EOL);
      if (fse.existsSync(commandFile)) {
        const original = await fse.readFile(commandFile, { encoding: 'utf-8' });
        if (original !== commandText) {
          await fse.writeFile(commandFile, commandText);
        }
      } else {
        await fse.mkdir(path.dirname(commandFile), { recursive: true });
        await fse.writeFile(commandFile, commandText);
      }

      subDurationEvent.stop();
      subDurationEvent.setLog(subTaskName, MetricLogType.INFO);
      const submitTaskName = `submit CMake task to work pool with ABI of ${abiFilter}`;
      const subToolDurationEvent = this.durationEvent.createSubEvent(submitTaskName, '');
      subToolDurationEvent.start();

      const fingerPrint = path.resolve(workDir, 'configure_fingerprint.json');
      const invalidationState = await createInvalidationState(
        FileId.getInstance(fingerPrint),
        await this.getInputFiles(workDir, abiFilter),
        this.getRequiredOutputFiles(workDir),
        this.getOptionalOutputFiles(workDir),
        this.getHardOutputFiles(workDir),
      );

      const shouldConfigure = invalidationState.shouldConfigure();
      ohosTrace.traceIncrement(this.moduleName, 'BUILD_NATIVE_WITH_CMAKE', !shouldConfigure);
      if (shouldConfigure) {
        if (!invalidationState.softConfigureOkay()) {
          // Clean CMakeCache.txt, clean output dir instead later.
          this._log.debug('Clean CMakeCache.txt due to outdated CMake configuration.');
          await fse.rm(path.resolve(workDir, 'CMakeCache.txt'), { recursive: true, force: true });
        }

        const cmakeCallback = async () => {
          const config = readCMakeFileApiReply(workDir, this.targetName, abiFilter, this.sdkInfo.getCmakeTool());

          const additionalFiles = config.extra?.join(os.EOL) ?? '';
          await fse.writeFile(path.resolve(workDir, 'additional_project_files.txt'), additionalFiles);
          await fse.writeFile(path.resolve(workDir, 'native_work_dir.txt'), workDir);
          await fse.writeFile(path.resolve(workDir, 'build_file_index.txt'), config.buildFiles.join(os.EOL));
          delete config.extra;

          await fse.writeJSON(path.resolve(workDir, HvigorNativeConfigFile), config);

          // 更新cmake脚本等输入文件
          invalidationState.addInputFiles(await this.getInputFiles(workDir, abiFilter));
          await invalidationState.updateFingerPrint();
          subToolDurationEvent.stop();
          subToolDurationEvent.setLog(submitTaskName, MetricLogType.INFO);
        };
        invalidationState.shouldConfigureMessages().forEach((message) => this._log.debug(message));

        CmakeUtil.mkCodeModelRequest(workDir);
        await this.executeCommand(command, workDir, cmakeCallback, [], subToolDurationEvent);
      } else {
        this._log.debug('CMake configuration was up-to-date');
        await invalidationState.updateFingerPrint();
        subToolDurationEvent.stop();
        subToolDurationEvent.setLog(submitTaskName, MetricLogType.INFO);
      }
    }
  }

  async getInputFiles(dir: string, abi: string) {
    const files = new Set<string>();

    // CMakeLists.txt 文件
    files.add(path.resolve(CmakeUtil.getCmakeListDir(this.moduleModel, this.targetName, this._nativeOption), 'CMakeLists.txt'));

    FileUtil.getAllFilesFromFolder(path.resolve(this.pathInfo.getNinjaWorkDir(), 'hvigor', abi)).forEach((cmake) => files.add(cmake));
    const configFile = path.resolve(dir, HvigorNativeConfigFile);
    if (!fse.existsSync(configFile)) {
      return [...files].map((file) => FileId.getInstance(file));
    }

    const config = await readHvigorNativeConfigFile(configFile);
    config?.buildFiles.forEach((file) => files.add(file));
    return [...files].map((file) => FileId.getInstance(file));
  }

  getOptionalOutputFiles(dir: string) {
    // 处理 compile_commands.json.bin
    return [
      path.resolve(dir, 'build.ninja'),
      path.resolve(dir, 'compile_commands.json'),
      path.resolve(dir, 'native_work_dir.txt'), // after cmake execution
      path.resolve(dir, 'build_file_index.txt'), // after cmake execution
      path.resolve(dir, 'additional_project_files.txt'), // parse file api
    ].map((file) => FileId.getInstance(file));
  }

  getRequiredOutputFiles(dir: string) {
    // after cmake execution
    return [path.resolve(dir, HvigorNativeConfigFile), path.resolve(dir, 'CMakeCache.txt')].map((file) => FileId.getInstance(file));
  }

  getHardOutputFiles(dir: string) {
    // after cmake execution
    return [path.resolve(dir, 'metadata_generation_command.txt')].map((file) => FileId.getInstance(file));
  }

  getToolchainFile(nativeCompiler: string | undefined, hmsAvailable: boolean) {
    let toolchain = '';
    if (nativeCompiler === CommonConst.BISHENG) {
      if (!hmsAvailable) {
        this._log.warn('Unknown compiler BiSheng, ' + 'The runtimeOS that supports the BiSheng can only be HarmonyOS');
      }
      toolchain = hmsAvailable ? this.sdkInfo.getHmsBiShengToolchainFile() : this.sdkInfo.getNativeToolchain();
    } else {
      toolchain = hmsAvailable ? this.sdkInfo.getHmsToolchainFile() : this.sdkInfo.getNativeToolchain();
    }
    if (!fse.existsSync(toolchain)) {
      this._log.warn(`Cannot find the file under the specified path ${toolchain}`);
      toolchain = fse.existsSync(this.sdkInfo.getHmsToolchainFile()) ? this.sdkInfo.getHmsToolchainFile() : this.sdkInfo.getNativeToolchain();
    }
    return toolchain;
  }

  buildCommand(abiFilter: string): string[] {
    const builder = new NativeCommandBuilder(this.sdkInfo.getCmakeTool());
    const outputDir = path.resolve(this.pathInfo.getIntermediatesCppOutPut(), abiFilter);
    const tempDir = path.resolve(this.pathInfo.getNinjaWorkDir(), abiFilter);
    const cmakeFindPath = path.resolve(this.pathInfo.getNinjaWorkDir(), 'hvigor', abiFilter);

    const buildType = this.targetService.getBuildMode();
    const hmsAvailable: boolean = this.targetData.isHarmonyOS() && this.compileApiVersion > 9;
    const nativeCompiler = this.targetService.getBuildOption()?.nativeCompiler;

    const toolchain = this.getToolchainFile(nativeCompiler, hmsAvailable);
    builder
      .addCmakeList(CmakeUtil.getCmakeListDir(this.moduleModel, this.targetName, this._nativeOption))
      .addTempFilePath(tempDir)
      .addOhosArch(abiFilter)
      .addOutputDir(outputDir)
      .addBuildType(buildType)
      .addNativeSdk(this.sdkInfo.getSdkNativeDir())
      .addSystemName('OHOS')
      .addOhosArchAbi(abiFilter)
      .exportCompileCommands('ON')
      .addToolChain(toolchain)
      .addGenerator('Ninja')
      .addMakeProgramPath(this.sdkInfo.getNativeNinjaTool())
      .addCMakeFindPath(cmakeFindPath)
      .addPackageFindScript(path.resolve(cmakeFindPath, NativeConst.PACKAGE_SUMMARY_FILE))
      .muteUnusedCliWarn();

    const cFlags = this._nativeOption?.cFlags;
    const cppFlags = this._nativeOption?.cppFlags;
    if (cFlags && cFlags.trim() !== '') {
      builder.addCFlags(cFlags);
    }
    if (cppFlags && cppFlags.trim() !== '') {
      builder.addCxxFlags(cppFlags);
    }
    if (hmsAvailable) {
      builder.addHMosNativeSdk(this.sdkInfo.getHmsNativeDir());
    }

    const nativeArgs = this.addNativeArgs(abiFilter);
    let commands = builder.build();
    commands = CmakeUtil.mergeCommandLine(commands, nativeArgs);
    this._log._printDebugCommand('Cmake', commands);
    return commands;
  }

  /**
   * 从hvigor参数设置和build-profile.json5配置文件中收集cmake参数，并返回
   * @param abiType 当前运行环境的abi类型
   */
  addNativeArgs(abiType: string) {
    const cmakeArgs: string[] = CmakeUtil.getCMakeArguments(this._nativeOption?.arguments);
    const builtinArgs = new Map([
      ['ohos-debug-asan', '-DOHOS_ENABLE_ASAN=ON'],
      ['ohos-enable-tsan', '-DOHOS_ENABLE_TSAN=ON'],
      ['ohos-enable-hwasan', '-DOHOS_ENABLE_HWASAN=ON'],
      ['ohos-enable-ubsan', '-DOHOS_ENABLE_UBSAN=ON'],
    ]);
    builtinArgs.forEach((val, key) => {
      if (key === 'ohos-enable-hwasan' && abiType !== 'arm64-v8a') {
        return;
      }
      const enable = hvigorCore.getExtraConfig().get(key);
      if (enable === 'true') {
        cmakeArgs.push(val);
      }
    });
    const cmakeArgsFlat = cmakeArgs.flatMap((item) => item.trim().split(' '));
    // 检查sdk是否支持输入的sanitizer参数
    this.checkSanArgsSupported(cmakeArgsFlat);
    // 当前不推荐使用超过一种sanitizer
    this.checkExclusiveSan(cmakeArgsFlat);

    return cmakeArgsFlat;
  }

  /**
   * 检查当前sdk版本是否支持传入的sanitizer参数
   * @param cmakeArgs 一系列cmake参数字符串所构成的数组
   */
  checkSanArgsSupported(cmakeArgs: string[]) {
    const apiMeta: ProjectBuildProfile.ProductApiMeta = this.targetData.getApiMeta();

    // 检查api配置项compatibleSdkVersion, 如果启用了asan并且 compatibleSdkVersion <= API9，不支持，
    const asanNotSupported = cmakeArgs.includes('-DOHOS_ENABLE_ASAN=ON') && apiMeta.compatibleSdkVersion.version <= ApiVersion.API_VERSION_9;
    if (asanNotSupported) {
      this._log.warn('To use ASan, first upgrade the SDK to API version 10 or later.');
    }

    // 如果启用了tsan, hwasan, ubsan, 并且compatibleSdkVersion <= API11，不支持
    const otherSanNotSupported =
      cmakeArgs.some((element) => BuildNativeWithCmake.sanAbilitySet.has(element) && element !== '-DOHOS_ENABLE_ASAN=ON') &&
      apiMeta.compatibleSdkVersion.version <= ApiVersion.API_VERSION_11;
    if (otherSanNotSupported) {
      this._log.warn('To use TSan, HWASan, or UBSan, first upgrade the SDK to API version 12 or later.');
    }
    
  }

  /**
   * 检查是否设置了多个相互排斥的san参数
   * @param cmakeArgs 一系列cmake参数字符串所构成的数组
   */
  checkExclusiveSan(cmakeArgs: string[]) {
    const exclusiveArguments = new Set(cmakeArgs.filter((element) => BuildNativeWithCmake.sanAbilitySet.has(element)));
    if (exclusiveArguments.size > 1) {
      this._log.warn('Sanitizers are mutually exclusive. Enable only one of them, such as ASan, TSan, HWASan, or UBSan, when using CMake.');
    }
    
  }
}
